#
#    Copyright (C) 2003-2005  James Tauber
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

"""
This module abstracts the access to file contents and file attributes.

Alternative backend data stores such as databases or version control systems
could be implemented by implementing an identical interface.

This implementation works by storing each "file" as a directory with the
extension ".ldv". The directory may then contain not only the content
(potentially in multiple forms) but additional metadata, annotations, etc.

@@@ document enclosures
@@@ document property indicies
"""


import os        
import re
import time
import cPickle as pickle
  
        
class LeonardoFileSystem:
    """
    The entry point to the file system.
    
    From here one can request file objects and their children as well as
    look up the property index.
    """
        

    def __init__(self, root):
        """
        Initialise the file system physically located at the given path.
        """
        
        self.root = root

        # check access to filesystem root
        if not os.access(self.root, os.R_OK):
            # @@@ improve response to user
            raise "can't access filesystem root"

        self.property_index_db = None
        

    def get(self, key):
        """
        Retrieve the file object of the given key.
        
        Raises a ValueError if the key is invalid.
        """
        
        if self.valid(key):
            return LeonardoFile(self, key)
        else:
            raise ValueError
            
    
    def valid(self, key):
        """
        Check whether the key is valid.
        
        Note, this doesn't mean the file exists, merely that the key
        is well-formed.
        """
        
        root = os.path.abspath(self.root)
        path = os.path.abspath(os.path.normpath(self.root + "/" + key))

        return path.startswith(root)


    def get_children(self, key):
        """
        Returns a tuple of:
          - list of directories that are children of the key
          - list of leonardo pages that are children of the key
          
        A ValueError is raised if the key is invalid.
        """

        if not self.valid(key):
            raise ValueError

        directories = []
        files = []
    
        # @@@ factor out commonalities with LeonardoFile
        d = self.root + key.strip("/")

        if os.path.isdir(d) and not d.endswith(".ldv"):
            for f in os.listdir(d):
                # @@@ factor out ignore list
                if os.path.isdir(d + "/" + f) and f not in [".svn"]:
                    if f.endswith(".ldv"):
                        # @@@ change to creation time
                        sort_key = os.path.getmtime(os.path.join(d, f))
                        
                        files.append((sort_key, f[:-4]))
                    else:
                        directories.append(f)
        directories.sort()
        directories.reverse()
        files.sort()
        files.reverse()
        files = [c[1] for c in files]
            
        return directories, files


    def find_property(self, property_name):
        """
        Find a property in the index.
        
        Returns a dictionary mapping values for that property to a list of
        keys to file objects with that value for the property.
        """

        property_pickle_filename = os.path.join(self.root, "property_pickle")
        if os.access(property_pickle_filename, os.F_OK):
            f = file(property_pickle_filename)
            property_index = pickle.load(f)
            f.close()
        else:
            property_index = {}

        return property_index.get(property_name, {})

    
    def list_properties(self):
        """
        List the properties in the index.
        """
        
        property_pickle_filename = os.path.join(self.root, "property_pickle")
        if os.access(property_pickle_filename, os.F_OK):
            f = file(property_pickle_filename)
            property_index = pickle.load(f)
            f.close()
        else:
            property_index = {}

        return property_index.keys()


    def add_property(self, key, name, value):
        """
        Add a property to the index.
        """

        property_pickle_filename = os.path.join(self.root, "property_pickle")
        if os.access(property_pickle_filename, os.F_OK):
            f = file(property_pickle_filename)
            property_index = pickle.load(f)
            f.close()
        else:
            property_index = {}
        
        if type(value) == list:
            for item in value:
                property_index.setdefault(name, {}).setdefault(item, []).append(key)
        else:
            property_index.setdefault(name, {}).setdefault(value, []).append(key)

        f = file(property_pickle_filename, "w")
        pickle.dump(property_index, f)
        f.close()
        

    def remove_property(self, key, name):
        """remove a property from the index"""

        property_pickle_filename = os.path.join(self.root, "property_pickle")
        if os.access(property_pickle_filename, os.F_OK):
            f = file(property_pickle_filename)
            property_index = pickle.load(f)
            f.close()
        else:
            property_index = {}

        x = property_index.get(name, {})

        for value in property_index.get(name, {}):
            y = x[value]
            if key in y:
                y.remove(key)

        property_index[name] = x

        f = file(property_pickle_filename, "w")
        pickle.dump(property_index, f)
        f.close()


    def add_time_property(self, key, name, value):
        """
        Add a key to the time index.
        """
        
        index_key = "__INDEX__/%s/%s" % (name, value)
        
        if self.valid(index_key):
            directory = self.root + index_key
            if not os.access(directory, os.F_OK):
                os.makedirs(directory)
            filename = directory + "/KEYS"
            if os.access(filename, os.F_OK):
                f = file(filename)
                keys = [line.strip() for line in f]
                f.close()
            else:
                keys = []
            if key not in keys:
                keys.append(key)
            f = file(filename, "w")
            for key in keys:
                f.write("%s\n" % key)
            f.close()
        else:
            raise ValueError


    def remove_time_property(self, key, name, value):
        """
        Remove a key from the time index.
        """
        
        index_key = "__INDEX__/%s/%s" % (name, value)
        
        if self.valid(index_key):
            directory = self.root + index_key
            if not os.access(directory, os.F_OK):
                os.makedirs(directory)
            filename = directory + "/KEYS"
            if os.access(filename, os.F_OK):
                f = file(filename)
                keys = [line.strip() for line in f]
                f.close()
            else:
                return
            if keys.count(key) > 0:
                keys.remove(key)
            f = file(filename, "w")
            for key in keys:
                f.write("%s\n" % key)
            f.close()
        else:
            raise ValueError





            

class Bundle:
    """
    A bundle is the parent class of file objects and enclosure objects.
    """


    def __init__(self):
        self.property_db = None
        

    def get_content_filename_(self, content_type):
        return self.get_directory_() + "__content__." + content_type


    def get_content_filename_glob_(self):
        return self.get_content_filename_("*")


    def get_content_files_(self):
        import glob
        return glob.glob(self.get_content_filename_glob_())
        

    def exists(self):
        files = self.get_content_files_()
        if files:
            return True
        else:
            return False
        

    def load_content_(self):
        files = self.get_content_files_()
        if files:
            f = file(files[0])    
            s = f.read()
            f.close()
            self.content = s


    def load_content_type_(self):
        files = self.get_content_files_()
        if files:
            self.content_type = files[0].split(".")[-1]
        else:
            self.content_type = None


    def get_content(self):
        if not self.content:
            self.load_content_()
        return self.content        


    def get_content_type(self):
        if not self.content_type:
            self.load_content_type_()
        return self.content_type
    

    def get_lastmod(self):
        lastmod = self.get_property("last_modified", -1)
        if lastmod == -1:
            files = self.get_content_files_()
            ST_MTIME = 8
            if files:
                lastmod = os.stat(files[0])[ST_MTIME]
            else:
                lastmod = -1
        return lastmod


    def get_creation_time(self):
        t = self.get_property("creation_time", "-1")
        return int(t)


    def set_content(self, content, content_type, update_lastmod=True):
        self.make_bundle_()
        self.content = content
        self.content_type = content_type
        path = self.get_content_filename_(content_type)
        f = file(path, "w")
        f.write(content)
        f.close()
        if update_lastmod:
            self.set_property("last_modified", time.time())
            self.set_time_property("last_modified", time.time())


    def delete(self):
        for root, dirs, files in os.walk(self.get_directory_(), topdown=False):
            for name in files:
                os.remove(os.path.join(root, name))
            for name in dirs:
                os.rmdir(os.path.join(root, name))
        os.rmdir(self.get_directory_())
        

    def make_bundle_(self):
        directory = self.get_directory_()
        if not os.access(directory, os.F_OK):
            os.makedirs(directory)
            self.set_property("creation_time", time.time())
            self.set_time_property("creation_time", time.time())


    def get_property(self, name, default=None):
        property_pickle_filename = self.get_directory_() + "property_pickle"
        if self.exists() and os.access(property_pickle_filename, os.R_OK):
            f = file(property_pickle_filename)
            property_pickle = pickle.load(f)
            f.close()
            return property_pickle.get(name, default)
        else:
            return default


    def get_properties(self):
        """Get a list of the properties (i.e. property names) on this file"""
        
        property_pickle_filename = self.get_directory_() + "property_pickle"
        if self.exists() and os.access(property_pickle_filename, os.R_OK):
            f = file(property_pickle_filename)
            property_pickle = pickle.load(f)
            f.close()
            return property_pickle.keys()
        else:
            return []


    def set_property(self, name, value):
        self.make_bundle_()

        property_pickle_filename = self.get_directory_() + "property_pickle"
        if os.access(property_pickle_filename, os.F_OK):
            f = file(property_pickle_filename)
            property_pickle = pickle.load(f)
            f.close()
        else:
            property_pickle = {}
        
        property_pickle[name] = value
        
        f = file(property_pickle_filename, "w")
        pickle.dump(property_pickle, f)
        f.close()


    def set_time_property(self, name, value):
        pass



def convert_time(t):
    return "%04d/%02d/%02d" % time.localtime(t)[:3]


class LeonardoFile(Bundle):

    
    def __init__(self, filesystem, key):

        Bundle.__init__(self)
        
        self.filesystem = filesystem
        self.key = key
        self.content = None
        self.content_type = None


    def get_directory_(self):
        return os.path.join(self.filesystem.root, self.key.strip("/") + ".ldv/")


    def set_property(self, name, value):
        Bundle.set_property(self, name, value)
        self.filesystem.remove_property(self.key, name)
        self.filesystem.add_property(self.key, name, value)


    def set_time_property(self, name, value):
        old_value = self.get_property(name)
        if old_value is not None:
            self.filesystem.remove_time_property(self.key, name, convert_time(old_value))
        Bundle.set_time_property(self, name, value)
        self.filesystem.add_time_property(self.key, name, convert_time(value))
        

    def delete(self):
        for name in self.get_properties():
            self.filesystem.remove_property(self.key, name)
        Bundle.delete(self)
        

    def enclosure(self, enctype, index):
        return Enclosure(self, enctype, index)
        
        
    def enclosures(self, enctype):
        #BNL: modified to reorder by creation date, since we can't
        #rely on the name or operating system.
        enc_list = []
        for d in os.listdir(self.get_directory_()):
            match = re.match("__(\w+)__(\d+)", d)
            if match and enctype == match.group(1):
                index = match.group(2)
                e=self.enclosure(enctype, index)
                sort_key=e.get_property('creation_time')
                enc_list.append((sort_key,e))
        enc_list.sort()
        return [i[1] for i in enc_list]
        


class Enclosure(Bundle):

    
    def __init__(self, parent, enctype, index):

        Bundle.__init__(self)
        
        self.parent = parent
        self.enctype = enctype
        self.index = index
        self.content = None
        self.content_type = None
        self.key = self.parent.key.strip("/") + "/__" + self.enctype + "__" + str(self.index)

    def get_directory_(self):
        return self.parent.filesystem.root + \
            self.parent.key.strip("/") + ".ldv/" + \
            "__" + self.enctype + "__" + str(self.index) + "/"
